/*
 * Copyright 2015 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.awaitility.pollinterval;


import java.time.Duration;
import java.util.function.Function;

import static org.awaitility.core.ForeverDuration.isForever;

/**
 * A poll interval that is generated by a function and a start duration. The function is free to do anything with the duration.
 * For example:
 * <pre>
 * await().with().pollInterval(iterative(duration -> duration.multiply(2)), Duration.FIVE_HUNDRED_MILLISECONDS).until(..);
 * </pre>
 * This generates a poll interval sequence that looks like this (ms): 500, 1000, 2000, 4000, 8000, 16000, ...
 * <p/>
 * Note that if the user specifies a poll delay this delay will take place <i>before</i> the first call to {@link #next(int, Duration)}.
 */
public class IterativePollInterval implements PollInterval {

    private final Function<Duration, Duration> function;
    private final Duration startDuration;

    /**
     * Generate an iterative poll interval based on the supplied function.
     *
     * @param function The function to use.
     */
    public IterativePollInterval(Function<Duration, Duration> function) {
        this(function, null, false);
    }

    /**
     * Generate a iterative poll interval based on the supplied function and start duration.
     *
     * @param function      The function to use.
     * @param startDuration The start duration (initial function value)
     */
    public IterativePollInterval(Function<Duration, Duration> function, Duration startDuration) {
        this(function, startDuration, true);
    }

    /**
     * Generate a iterative poll interval based on the supplied function and start duration.
     *
     * @param function      The function to use.
     * @param startDuration The start duration (initial function value)
     */
    private IterativePollInterval(Function<Duration, Duration> function, Duration startDuration, boolean startDurationExplicitlyDefined) {
        if (function == null) {
            throw new IllegalArgumentException("Function<Duration, Duration> cannot be null");
        }
        if (startDurationExplicitlyDefined && startDuration == null) {
            throw new IllegalArgumentException("Start duration cannot be null");
        } else if (startDurationExplicitlyDefined && isForever(startDuration)) {
            throw new IllegalArgumentException("Cannot use a poll interval of length 'forever'");
        }
        this.function = function;
        this.startDuration = startDuration;
    }

    /**
     * Generate the next Duration based on the supplied function. If you've specified a start duration explicitly then
     * this start duration will override the value of <code>previousDuration</code> when <code>pollCount</code> is 1 (i.e. the poll delay).
     *
     * @param pollCount        The number of times the condition has been polled (evaluated). Always a positive integer.
     * @param previousDuration The duration of the previously returned poll interval.
     * @return The duration of the next poll interval
     */
    public Duration next(int pollCount, Duration previousDuration) {
        final Duration durationToUse;
        if (pollCount == 1 && startDuration != null) {
            durationToUse = startDuration;
        } else {
            durationToUse = previousDuration;
        }
        return function.apply(durationToUse);
    }

    /**
     * Syntactic sugar for creating a {@link IterativePollInterval}.
     *
     * @param function The function to use
     * @return A new instance of {@link IterativePollInterval}
     */
    public static IterativePollInterval iterative(Function<Duration, Duration> function) {
        return new IterativePollInterval(function);
    }

    /**
     * Syntactic sugar for creating a {@link IterativePollInterval}.
     *
     * @param function      The function to use
     * @param startDuration The start duration (initial function value)
     * @return A new instance of {@link IterativePollInterval}
     */
    public static IterativePollInterval iterative(Function<Duration, Duration> function, Duration startDuration) {
        return new IterativePollInterval(function, startDuration);
    }

    /**
     * Syntactic sugar
     *
     * @return The same of instance of {@link IterativePollInterval}
     */
    public IterativePollInterval with() {
        return this;
    }

    /**
     * Set the start duration of this poll interval
     *
     * @return A new of instance of {@link IterativePollInterval} with the given start duration
     */
    public IterativePollInterval startDuration(Duration duration) {
        return new IterativePollInterval(function, duration);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof IterativePollInterval)) return false;

        IterativePollInterval that = (IterativePollInterval) o;

        return function.equals(that.function) && startDuration.equals(that.startDuration);

    }

    @Override
    public int hashCode() {
        int result = function.hashCode();
        result = 31 * result + startDuration.hashCode();
        return result;
    }

    @Override
    public String toString() {
        return "IterativePollInterval{" +
                "function=" + function +
                ", startDuration=" + startDuration +
                '}';
    }
}